package com.appspot.bibtable.model;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.Inheritance;
import javax.jdo.annotations.InheritanceStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

import com.appspot.bibtable.model.annotation.AttributeValue;
import com.appspot.bibtable.model.annotation.Input;
import com.appspot.bibtable.model.annotation.InputType;
import com.appspot.bibtable.model.annotation.Output;
import com.appspot.bibtable.model.format.CollectionFormatter;
import com.appspot.bibtable.model.format.DefaultInputParser;
import com.appspot.bibtable.model.format.DefaultOutputFormatter;
import com.appspot.bibtable.model.format.InputParser;
import com.appspot.bibtable.model.format.ListParser;
import com.appspot.bibtable.model.format.OutputFormatter;
import com.appspot.bibtable.model.format.YearFormatter;
import com.appspot.bibtable.model.format.YearParser;
import com.appspot.bibtable.util.ReflectionUtility;
import com.appspot.bibtable.util.StringUtility;
import com.google.appengine.api.datastore.Key;

@PersistenceCapable(detachable = "true")
@Inheritance(strategy = InheritanceStrategy.SUBCLASS_TABLE)
public abstract class Reference
{
	private static final Logger logger = Logger.getLogger(Reference.class.getName());

	@PrimaryKey
	@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
	// @Input(name = @AttributeValue(attribute = "key", display = "Key"), type = InputType.READONLY_TEXT)
	// @Output(formatter = KeyFormatter.class)
	private Key key;

	@Persistent
	@Input(name = @AttributeValue(attribute = "title", display = "Title"))
	@Output
	private String title;

	@Persistent(defaultFetchGroup = "true")
	@Input(name = @AttributeValue(attribute = "author", display = "Author"), parser = ListParser.class, elementParserRequired = true, document = "Surname before forename; surname must not be initials and forename can be. Semicolon-separating for more than one. can be put in brackets []. (e.g. Smith J. M.; von Neumann John)")
	@Output(formatter = CollectionFormatter.class)
	private List<String> author;

	@Persistent
	@Input(name = @AttributeValue(attribute = "published", display = "Published"), parser = YearParser.class, document = "4-digit year. (e.g. 2010)")
	@Output(formatter = YearFormatter.class)
	private Date published;

	@Persistent
	@Input(name = @AttributeValue(attribute = "language", display = "Language"), type = InputType.COMBOBOX, values = { @AttributeValue(attribute = "en", display = "English") })
	@Output
	private String language;

	protected Reference()
	{
		author = new ArrayList<String>();
	}

	protected Reference(Reference reference)
	{
		this.title = reference.title;
		this.author = reference.author;
		this.published = reference.published;
		this.language = reference.language;
	}

	public final Key getKey()
	{
		return key;
	}

	public final void setKey(Key key)
	{
		this.key = key;
	}

	public String getTitle()
	{
		return title;
	}

	public void setTitle(String title)
	{
		this.title = title;
	}

	public Date getPublished()
	{
		return published;
	}

	public void setPublished(Date publish)
	{
		this.published = publish;
	}

	public String getLanguage()
	{
		return language;
	}

	public void setLanguage(String language)
	{
		this.language = language;
	}

	public List<String> getAuthor()
	{
		return author;
	}

	public int countAuthor()
	{
		return getAuthor().size();
	}

	public String getAuthor(int index)
	{
		return getAuthor().get(index);
	}

	protected void setAuthor(List<String> author)
	{
		this.author = author;
	}

	public void addAuthor(String author)
	{
		getAuthor().add(author);
	}

	public void addAuthor(int index, String author)
	{
		getAuthor().add(index, author);
	}

	public void removeAuthor(String author)
	{
		getAuthor().remove(author);
	}

	public String getType()
	{
		return StringUtility.camelcaseToPhrase(getClass().getSimpleName());
	}

	public String getValue(Field field)
	{
		field.setAccessible(true);
		Output output = field.getAnnotation(Output.class);

		String value = null;
		try
		{
			Object object = field.get(this);
			OutputFormatter formatter = new DefaultOutputFormatter();
			if (output != null)
			{
				try
				{
					if (output.elementFormatterRequired())
					{
						Constructor<? extends OutputFormatter> elementFormatterConstructor = output.elementFormatter().getConstructor();
						OutputFormatter elementFormatter = elementFormatterConstructor.newInstance();
						Constructor<? extends OutputFormatter> constructor = output.formatter().getConstructor(OutputFormatter.class);
						formatter = constructor.newInstance(elementFormatter);
					}
					else
					{
						Constructor<? extends OutputFormatter> constructor = output.formatter().getConstructor();
						formatter = constructor.newInstance();
					}
				}
				catch (Exception ex)
				{
					logger.severe(ex.getMessage());
				}
			}
			value = formatter.format(object);
		}
		catch (IllegalAccessException ex)
		{
			logger.warning(ex.getMessage());
		}
		return value;
	}

	public void setValue(Field field, String value)
	{
		field.setAccessible(true);
		Input input = field.getAnnotation(Input.class);

		try
		{
			InputParser parser = new DefaultInputParser();
			if (input != null)
			{
				try
				{
					if (input.elementParserRequired())
					{
						Constructor<? extends InputParser> elementParserConstructor = input.elementParser().getConstructor();
						InputParser elementParser = elementParserConstructor.newInstance();
						Constructor<? extends InputParser> constructor = input.parser().getConstructor(InputParser.class);
						parser = constructor.newInstance(elementParser);
					}
					else
					{
						Constructor<? extends InputParser> constructor = input.parser().getConstructor();
						parser = constructor.newInstance();
					}
				}
				catch (Exception ex)
				{
					logger.severe(ex.getMessage());
				}
			}
			Object parsedValue = parser.parse(value);
			field.set(this, parsedValue);
		}
		catch (IllegalAccessException ex)
		{
			logger.warning(ex.getMessage());
		}
	}

	@Override
	public int hashCode()
	{
		final int prime = 31;
		int result = 1;
		result = prime * result + ((key == null) ? 0 : key.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Reference other = (Reference)obj;
		if (key == null)
		{
			if (other.key != null)
				return false;
		}
		else if (!key.equals(other.key))
			return false;
		return true;
	}

	@Override
	public String toString()
	{
		Field[] fields = ReflectionUtility.getAllFields(getClass());

		StringBuilder builder = new StringBuilder();
		for (Field field : fields)
		{
			appendField(field, builder);
		}

		int length = builder.length();
		if (builder.length() > 0)
		{
			builder.delete(length - 2, length);
			builder.insert(0, "{ ").append(" }");
		}
		else
		{
			builder.append("{}");
		}
		builder.insert(0, getType());
		return builder.toString();
	}

	private void appendField(Field field, StringBuilder builder)
	{
		Output output = field.getAnnotation(Output.class);
		if (output == null)
		{
			return;
		}

		String value = getValue(field);
		if (value != null)
		{
			builder.append(field.getName()).append(": ").append(value).append("; ");
		}
	}
}
